Add Worker-safe create manifest export#475
Conversation
📝 WalkthroughWalkthroughAdds a ChangesEdge Export and Manifest Generation
Sequence DiagramsequenceDiagram
participant User as User/Worker
participant edge as `@tanstack/create/edge`
participant finalizeAddOns
participant createMemoryEnvironment
participant createApp
User->>edge: getFrameworkById('react')
edge-->>User: Framework (from compiled manifest)
User->>finalizeAddOns: framework, mode, chosenAddOnIDs[]
finalizeAddOns-->>User: resolved AddOn[]
User->>createMemoryEnvironment: returnPathsRelativeTo
createMemoryEnvironment-->>User: { environment, output }
User->>createApp: environment, options
createApp->>createApp: writeFiles (render templates from manifest)
createApp->>createApp: seedEnvValues / writeEnvExample
createApp->>createApp: runCommandsAndInstallDependencies
createApp-->>User: void (environment.output.files populated)
User->>User: output.files → ZIP / response
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
eslint.config.jsParsing error: project was set to packages/create/tests/edge-import.test.tsParsing error: "parserOptions.project" has been provided for packages/create/tests/edge-manifest.test.tsParsing error: "parserOptions.project" has been provided for Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (1)
docs/cli-reference.md (1)
47-85: 💤 Low valueClear Worker-safe API documentation.
The example demonstrates the complete workflow for programmatic app generation in edge runtimes. The code is concise and ready to use.
📝 Optional: Mention the
includeExamplesoptionUsers might want to know about the
includeExamplesoption to control example code generation:await createApp(environment, { projectName: 'app', targetDir: '/app', framework, mode: 'file-router', typescript: true, tailwind: true, packageManager: 'pnpm', git: false, install: false, intent: false, chosenAddOns, addOnOptions, + includeExamples: false, })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/cli-reference.md` around lines 47 - 85, Add documentation for the includeExamples option in the Worker-safe programmatic generation example. Include the includeExamples property in the options object passed to the createApp function call to demonstrate how users can control example code generation. Add a brief comment or explanation in the documentation explaining that this option controls whether example files are generated during app creation, helping users understand they can set it to true or false based on their needs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/create/package.json`:
- Around line 27-33: The test:coverage script is missing the manifest generation
prerequisite that other scripts include. Add "npm run generate-manifest &&" to
the beginning of the test:coverage script command to ensure the generated
manifest file exists before running coverage tests, following the same pattern
used in the build, dev, test, and test:watch scripts.
In `@packages/create/scripts/generate-manifest.mjs`:
- Around line 76-83: The readdirSync call in the visit function iterates over
directory entries in filesystem order, which is non-deterministic across
different environments and causes inconsistent manifest generation. Sort the
array returned by readdirSync before iterating over it with the for loop. Apply
the same sorting fix to the other location mentioned (around lines 113-116)
where readdirSync is also used without sorting to ensure deterministic manifest
ordering regardless of filesystem.
In `@packages/create/src/edge-add-ons.ts`:
- Around line 36-38: The code fetches remote add-ons via
loadRemoteAddOn(addOnID) without validating the URL, creating an SSRF
vulnerability if addOnID originates from external input. Additionally,
loadRemoteAddOn likely processes HTTP responses as JSON without checking status
codes. Add URL validation before calling loadRemoteAddOn to reject localhost
addresses, private IP ranges, and cloud metadata endpoints. Modify
loadRemoteAddOn to check the HTTP response status code and only parse JSON for
2xx responses, rejecting error responses. Apply these same hardening fixes to
the similar remote add-on loading code at lines 76-79.
In `@packages/create/src/edge-config-file.ts`:
- Around line 17-24: The return object in this function uses the `...rest`
spread operator which inadvertently includes sensitive fields like
`envVarValues` and `addOns` in the persisted config file, creating a security
risk. Instead of spreading rest, explicitly list only the safe fields that
should be persisted in the edge config file. Destructure out `envVarValues` and
`addOns` from the options parameter along with the other fields already being
destructured, then return only the intended config fields (version, framework as
an id, chosenAddOns as ids mapped from the objects, and starter as an id if
present) without using the spread operator for remaining fields.
In `@packages/create/src/edge-create-app.ts`:
- Around line 565-567: The cd instruction string being printed uses the
incorrect variable `projectName` instead of `targetDir`. In the conditional
expression around line 565-567 in edge-create-app.ts, replace the `projectName`
reference with `targetDir` in the template string to ensure users are directed
to the correct output directory, which may differ from the project name when the
output directory is specified separately.
- Around line 445-446: The regex pattern in the RegExp constructor directly
interpolates the key variable without escaping, which means regex metacharacters
in the key will be treated as operators rather than literals. Before
constructing the pattern, escape the key using a regex escape utility function
(such as escaping characters like . * + ? [ ] ( ) etc.) so that special
characters are treated as literal characters in the pattern match. This prevents
keys with regex metacharacters from unintentionally changing the regex behavior.
- Around line 396-397: The calls to installShadcnComponents and setupIntent
functions are executing without checking whether the install flag is disabled in
options. Add a conditional check before both function calls to verify that
install is not set to false (i.e., check if options.install is not explicitly
false) to prevent running these networked commands when skip-install mode is
enabled.
In `@packages/create/src/edge-environment.ts`:
- Around line 37-42: The startRun method clears the output object but does not
reset the persistent in-memory files map, causing state leakage when the same
environment instance is reused for multiple runs. In the startRun method, in
addition to clearing the output properties, also reset the files map to an empty
object to ensure each run starts with a clean state and prevent carryover of
files from previous runs.
- Around line 98-103: The `exists` and `isDirectory` methods fail to handle root
path cases (represented as `.`) because `hasDirectory` expects a `./` prefix
that normalized file keys never contain. Add explicit checks in both the
`exists` and `isDirectory` functions to detect when the input path represents
the root directory (normalize to `.` or empty string after normalization) and
return `true` immediately for these cases, since the root always exists and is a
directory. Only call `hasDirectory` for non-root paths.
In `@packages/create/src/edge-file-helpers.ts`:
- Around line 38-43: In the relativePath function, normalize the to parameter
the same way as from by applying the backslash-to-forward-slash replacement
before processing it. Currently, from is normalized with replace(/\\/g, '/') but
to is used directly without this normalization. Add a line to normalize to by
replacing backslashes with forward slashes, similar to how from is normalized,
to ensure consistent path handling regardless of whether paths use backslashes
or forward slashes as separators.
- Around line 18-25: The toCleanPath function uses startsWith for path matching
without ensuring path segment boundaries, which causes incorrect slicing when
there's a partial match (e.g., normalizing /apple/file with base /app
incorrectly yields le/file). Fix this by adding a boundary check after the
startsWith match in both the normalizedPath.startsWith(normalizedBase) condition
and the pathNoDrive.startsWith(baseNoDrive) condition. Ensure the character
immediately following the matched base is either a path separator or the end of
the string before performing the slice operation.
In `@packages/create/src/edge-package-json.ts`:
- Around line 114-118: The additions array in the createPackageJSON function is
hardcoding the inclusion of TypeScript and Tailwind optional packages regardless
of user selection. Modify the additions array to conditionally include
options.framework.optionalPackages.typescript and
options.framework.optionalPackages.tailwindcss only when those options have been
explicitly selected by the user (check options.typescript and
options.tailwindcss flags). Similarly, update any template rendering logic
(referenced in lines 131-137) to use the actual selected options instead of
hardcoded TypeScript/TSX values, ensuring the correct file extensions and
configurations are used based on what the user chose.
- Around line 153-160: The catch block in the try-catch statement around
JSON.parse(render(addOn.packageTemplate, templateValues)) only logs the error
and continues execution, which allows incomplete packageJSON data to persist and
cause failures later. After logging the error with console.error, throw the
caught error to fail fast at the source of the problem rather than silently
continuing with invalid addOnPackageJSON data.
In `@packages/create/src/edge-template-file.ts`:
- Line 183: The file.replace method in the line with convertDotFilesAndPaths
will remove all instances of `.ejs` from the filename, not just the file
extension at the end. Fix this by modifying the replace call to use a regular
expression pattern that matches `.ejs` only at the end of the string using the
`$` anchor, so that filenames containing `.ejs` in the middle are left
unchanged. This ensures only the actual `.ejs` template extension is removed.
- Around line 127-133: The templateValues object in the edge-template-file.ts
file has hardcoded TypeScript values (typescript: true, js: 'ts', jsx: 'tsx')
that ignore the user's language preference. Replace these hardcoded values with
conditional values based on options.typescript: set typescript to
options.typescript, js to either 'ts' or 'js' depending on whether
options.typescript is true, and jsx to either 'tsx' or 'jsx' depending on
whether options.typescript is true. This ensures the correct file extensions and
flags are used for both TypeScript and JavaScript projects.
---
Nitpick comments:
In `@docs/cli-reference.md`:
- Around line 47-85: Add documentation for the includeExamples option in the
Worker-safe programmatic generation example. Include the includeExamples
property in the options object passed to the createApp function call to
demonstrate how users can control example code generation. Add a brief comment
or explanation in the documentation explaining that this option controls whether
example files are generated during app creation, helping users understand they
can set it to true or false based on their needs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: affd348c-b1c2-4ad1-a65e-e44c84b3871c
📒 Files selected for processing (22)
.changeset/slow-buses-build.md.gitignoredocs/cli-reference.mdeslint.config.jspackages/create/package.jsonpackages/create/scripts/generate-manifest.mjspackages/create/src/edge-add-ons.tspackages/create/src/edge-config-file.tspackages/create/src/edge-create-app.tspackages/create/src/edge-environment.tspackages/create/src/edge-file-helpers.tspackages/create/src/edge-frameworks.tspackages/create/src/edge-package-json.tspackages/create/src/edge-path.tspackages/create/src/edge-render.tspackages/create/src/edge-template-file.tspackages/create/src/edge.tspackages/create/src/manifest-types.tspackages/create/src/manifest.tspackages/create/src/types.tspackages/create/tests/edge-import.test.tspackages/create/tests/edge-manifest.test.ts
| "scripts": { | ||
| "build": "tsc && npm run copy-assets", | ||
| "build": "npm run generate-manifest && tsc && npm run copy-assets", | ||
| "generate-manifest": "node ./scripts/generate-manifest.mjs", | ||
| "copy-assets": "node -e \"const fs=require('fs');const path=require('path');function copyDir(src,dest){if(!fs.existsSync(dest))fs.mkdirSync(dest,{recursive:true});for(const entry of fs.readdirSync(src,{withFileTypes:true})){const srcPath=path.join(src,entry.name);const destPath=path.join(dest,entry.name);if(entry.isDirectory())copyDir(srcPath,destPath);else fs.copyFileSync(srcPath,destPath)}}['react','solid'].forEach(fw=>{['add-ons','toolchains','hosts','examples','project'].forEach(dir=>{const src='src/frameworks/'+fw+'/'+dir;const dest='dist/frameworks/'+fw+'/'+dir;if(fs.existsSync(src))copyDir(src,dest)})})\"", | ||
| "dev": "tsc --watch", | ||
| "test": "eslint ./src && vitest run", | ||
| "test:watch": "vitest", | ||
| "dev": "npm run generate-manifest && tsc --watch", | ||
| "test": "npm run generate-manifest && eslint ./src && vitest run", | ||
| "test:watch": "npm run generate-manifest && vitest", |
There was a problem hiding this comment.
test:coverage should also run manifest generation in clean environments.
This block correctly adds codegen pre-steps for build/dev/test flows, but coverage still misses that prerequisite and can fail when src/generated/create-manifest.ts is absent.
Proposed fix
- "test:coverage": "vitest run --coverage"
+ "test:coverage": "npm run generate-manifest && vitest run --coverage"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/package.json` around lines 27 - 33, The test:coverage script
is missing the manifest generation prerequisite that other scripts include. Add
"npm run generate-manifest &&" to the beginning of the test:coverage script
command to ensure the generated manifest file exists before running coverage
tests, following the same pattern used in the build, dev, test, and test:watch
scripts.
| for (const entry of readdirSync(dir, { withFileTypes: true })) { | ||
| const file = resolve(dir, entry.name) | ||
| if (entry.isDirectory()) { | ||
| visit(file) | ||
| } else { | ||
| files[toCleanPath(file, baseDir)] = readTemplateFile(file) | ||
| } | ||
| } |
There was a problem hiding this comment.
Sort directory entries before iterating to keep manifest generation deterministic.
Line [76] and Line [113] depend on raw readdirSync iteration order, which is not guaranteed to be stable across filesystems. That can produce different manifest/add-on ordering between environments.
Proposed fix
function findFilesRecursively(baseDir) {
const files = {}
if (!existsSync(baseDir)) {
return files
}
function visit(dir) {
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
+ a.name.localeCompare(b.name),
+ )
+ for (const entry of entries) {
const file = resolve(dir, entry.name)
if (entry.isDirectory()) {
visit(file)
} else {
files[toCleanPath(file, baseDir)] = readTemplateFile(file)
@@
- for (const entry of readdirSync(addOnsBase, { withFileTypes: true })) {
+ const entries = readdirSync(addOnsBase, { withFileTypes: true }).sort(
+ (a, b) => a.name.localeCompare(b.name),
+ )
+ for (const entry of entries) {
if (!entry.isDirectory()) {
continue
}Also applies to: 113-116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/scripts/generate-manifest.mjs` around lines 76 - 83, The
readdirSync call in the visit function iterates over directory entries in
filesystem order, which is non-deterministic across different environments and
causes inconsistent manifest generation. Sort the array returned by readdirSync
before iterating over it with the for loop. Apply the same sorting fix to the
other location mentioned (around lines 113-116) where readdirSync is also used
without sorting to ensure deterministic manifest ordering regardless of
filesystem.
| } else if (addOnID.startsWith('http')) { | ||
| addOn = await loadRemoteAddOn(addOnID) | ||
| addOns.push(addOn) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the edge-add-ons.ts file
find . -name "edge-add-ons.ts" -type fRepository: TanStack/cli
Length of output: 94
🏁 Script executed:
# Read the edge-add-ons.ts file to understand the context
cat -n packages/create/src/edge-add-ons.tsRepository: TanStack/cli
Length of output: 4727
🏁 Script executed:
# Check file size first
wc -l packages/create/src/edge-add-ons.tsRepository: TanStack/cli
Length of output: 96
🏁 Script executed:
# Read the entire file
cat packages/create/src/edge-add-ons.tsRepository: TanStack/cli
Length of output: 3761
🏁 Script executed:
# Search for usages of finalizeAddOns to understand if chosenAddOnIDs comes from external input
rg "finalizeAddOns" --type ts --type js -B 2 -A 2Repository: TanStack/cli
Length of output: 5808
🏁 Script executed:
# Search for callers of loadRemoteAddOn and finalizeAddOns
rg "finalizeAddOns|loadRemoteAddOn" --type ts --type js -B 3 -A 3Repository: TanStack/cli
Length of output: 11282
🏁 Script executed:
# Check if there's also a non-edge add-ons.ts with similar code
head -100 packages/create/src/add-ons.tsRepository: TanStack/cli
Length of output: 2904
🏁 Script executed:
# Look at loadRemoteAddOn in packages/create/src/custom-add-ons/add-on.ts to see if different
cat packages/create/src/custom-add-ons/add-on.tsRepository: TanStack/cli
Length of output: 8273
Harden remote add-on URL fetching (SSRF risk in hosted edge flows).
Any http* add-on ID is fetched directly without URL validation. If chosenAddOnIDs originates from external request input (possible in hosted/edge deployments), this enables SSRF attacks against localhost services, cloud metadata endpoints, or internal IP ranges. Additionally, non-2xx responses trigger JSON parsing without status checks, allowing error responses to be processed as add-on data.
Suggested hardening baseline
export async function loadRemoteAddOn(url: string): Promise<AddOn> {
- const response = await fetch(url)
+ const parsed = new URL(url)
+ if (parsed.protocol !== 'https:') {
+ throw new Error(`Only https add-on URLs are allowed: ${url}`)
+ }
+ if (['localhost', '127.0.0.1', '::1'].includes(parsed.hostname)) {
+ throw new Error(`Local add-on URLs are not allowed: ${url}`)
+ }
+
+ const response = await fetch(parsed.toString())
+ if (!response.ok) {
+ throw new Error(`Failed to fetch add-on: ${url} (${response.status})`)
+ }
const jsonContent = await response.json()Also applies to: 76-79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-add-ons.ts` around lines 36 - 38, The code fetches
remote add-ons via loadRemoteAddOn(addOnID) without validating the URL, creating
an SSRF vulnerability if addOnID originates from external input. Additionally,
loadRemoteAddOn likely processes HTTP responses as JSON without checking status
codes. Add URL validation before calling loadRemoteAddOn to reject localhost
addresses, private IP ranges, and cloud metadata endpoints. Modify
loadRemoteAddOn to check the HTTP response status code and only parse JSON for
2xx responses, rejecting error responses. Apply these same hardening fixes to
the similar remote add-on loading code at lines 76-79.
| const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options | ||
| return { | ||
| ...rest, | ||
| version: 1, | ||
| framework: framework.id, | ||
| chosenAddOns: chosenAddOns.map((addOn) => addOn.id), | ||
| starter: options.starter?.id ?? undefined, | ||
| } |
There was a problem hiding this comment.
Avoid persisting secret/runtime-only fields into the config file.
Using ...rest here includes fields like envVarValues (potential secrets) and addOns in .cta.json, which violates the intended persisted shape and increases leak risk.
Suggested fix
function createPersistedOptions(options: Options): PersistedOptions {
- const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options
+ const {
+ chosenAddOns,
+ framework,
+ starter,
+ addOns: _addOns,
+ envVarValues: _envVarValues,
+ targetDir: _targetDir,
+ ...rest
+ } = options
return {
...rest,
version: 1,
framework: framework.id,
chosenAddOns: chosenAddOns.map((addOn) => addOn.id),
- starter: options.starter?.id ?? undefined,
+ starter: starter?.id ?? undefined,
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options | |
| return { | |
| ...rest, | |
| version: 1, | |
| framework: framework.id, | |
| chosenAddOns: chosenAddOns.map((addOn) => addOn.id), | |
| starter: options.starter?.id ?? undefined, | |
| } | |
| function createPersistedOptions(options: Options): PersistedOptions { | |
| const { | |
| chosenAddOns, | |
| framework, | |
| starter, | |
| addOns: _addOns, | |
| envVarValues: _envVarValues, | |
| targetDir: _targetDir, | |
| ...rest | |
| } = options | |
| return { | |
| ...rest, | |
| version: 1, | |
| framework: framework.id, | |
| chosenAddOns: chosenAddOns.map((addOn) => addOn.id), | |
| starter: starter?.id ?? undefined, | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-config-file.ts` around lines 17 - 24, The return
object in this function uses the `...rest` spread operator which inadvertently
includes sensitive fields like `envVarValues` and `addOns` in the persisted
config file, creating a security risk. Instead of spreading rest, explicitly
list only the safe fields that should be persisted in the edge config file.
Destructure out `envVarValues` and `addOns` from the options parameter along
with the other fields already being destructured, then return only the intended
config fields (version, framework as an id, chosenAddOns as ids mapped from the
objects, and starter as an id if present) without using the spread operator for
remaining fields.
| await installShadcnComponents(environment, options.targetDir, options) | ||
| await setupIntent(environment, options.targetDir, options) |
There was a problem hiding this comment.
Honor install: false for post-generation command execution.
When install is disabled, the flow still runs shadcn@latest add and @tanstack/intent install --map, which triggers networked command execution despite skip-install mode.
Suggested fix
- await installShadcnComponents(environment, options.targetDir, options)
- await setupIntent(environment, options.targetDir, options)
+ if (options.install !== false) {
+ await installShadcnComponents(environment, options.targetDir, options)
+ await setupIntent(environment, options.targetDir, options)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await installShadcnComponents(environment, options.targetDir, options) | |
| await setupIntent(environment, options.targetDir, options) | |
| if (options.install !== false) { | |
| await installShadcnComponents(environment, options.targetDir, options) | |
| await setupIntent(environment, options.targetDir, options) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-create-app.ts` around lines 396 - 397, The calls to
installShadcnComponents and setupIntent functions are executing without checking
whether the install flag is disabled in options. Add a conditional check before
both function calls to verify that install is not set to false (i.e., check if
options.install is not explicitly false) to prevent running these networked
commands when skip-install mode is enabled.
| const normalized = from.replace(/\\/g, '/') | ||
| const cleanedFrom = normalized.startsWith('./') | ||
| ? normalized.slice(2) | ||
| : normalized | ||
| const cleanedTo = to.startsWith('./') ? to.slice(2) : to | ||
|
|
There was a problem hiding this comment.
Normalize to in relativePath the same way as from.
Line [38] normalizes from, but Line [42] uses to raw. With backslash paths, segment splitting and extension stripping become inconsistent and can produce incorrect import paths.
Proposed fix
export function relativePath(
from: string,
to: string,
stripExtension: boolean = false,
) {
const normalized = from.replace(/\\/g, '/')
+ const normalizedTo = to.replace(/\\/g, '/')
const cleanedFrom = normalized.startsWith('./')
? normalized.slice(2)
: normalized
- const cleanedTo = to.startsWith('./') ? to.slice(2) : to
+ const cleanedTo = normalizedTo.startsWith('./')
+ ? normalizedTo.slice(2)
+ : normalizedTo
@@
- const target = stripExtension ? to.replace(extnamePath(to), '') : to
+ const target = stripExtension
+ ? normalizedTo.replace(extnamePath(normalizedTo), '')
+ : normalizedToAlso applies to: 61-61
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-file-helpers.ts` around lines 38 - 43, In the
relativePath function, normalize the to parameter the same way as from by
applying the backslash-to-forward-slash replacement before processing it.
Currently, from is normalized with replace(/\\/g, '/') but to is used directly
without this normalization. Add a line to normalize to by replacing backslashes
with forward slashes, similar to how from is normalized, to ensure consistent
path handling regardless of whether paths use backslashes or forward slashes as
separators.
| const additions: Array<Record<string, unknown> | undefined> = [ | ||
| options.framework.optionalPackages.typescript, | ||
| options.framework.optionalPackages.tailwindcss, | ||
| options.mode ? options.framework.optionalPackages[options.mode] : undefined, | ||
| ] |
There was a problem hiding this comment.
Use the selected TypeScript/Tailwind options instead of hardcoded values.
createPackageJSON currently always merges TS/Tailwind optional packages and always renders add-on package templates as TS/TSX. That produces incorrect package.json output for JS/no-tailwind configurations.
Suggested fix
- const additions: Array<Record<string, unknown> | undefined> = [
- options.framework.optionalPackages.typescript,
- options.framework.optionalPackages.tailwindcss,
- options.mode ? options.framework.optionalPackages[options.mode] : undefined,
- ]
+ const additions: Array<Record<string, unknown> | undefined> = [
+ options.typescript ? options.framework.optionalPackages.typescript : undefined,
+ options.tailwind ? options.framework.optionalPackages.tailwindcss : undefined,
+ options.mode ? options.framework.optionalPackages[options.mode] : undefined,
+ ]
@@
- typescript: true,
- tailwind: true,
- js: 'ts',
- jsx: 'tsx',
+ typescript: options.typescript === true,
+ tailwind: options.tailwind === true,
+ js: options.typescript ? 'ts' : 'js',
+ jsx: options.typescript ? 'tsx' : 'jsx',Also applies to: 131-137
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-package-json.ts` around lines 114 - 118, The
additions array in the createPackageJSON function is hardcoding the inclusion of
TypeScript and Tailwind optional packages regardless of user selection. Modify
the additions array to conditionally include
options.framework.optionalPackages.typescript and
options.framework.optionalPackages.tailwindcss only when those options have been
explicitly selected by the user (check options.typescript and
options.tailwindcss flags). Similarly, update any template rendering logic
(referenced in lines 131-137) to use the actual selected options instead of
hardcoded TypeScript/TSX values, ensuring the correct file extensions and
configurations are used based on what the user chose.
| try { | ||
| addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues)) | ||
| } catch (error) { | ||
| console.error( | ||
| `Error processing package.json.ejs for add-on ${addOn.id}:`, | ||
| error, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Fail fast when an add-on packageTemplate cannot be rendered/parsed.
The current catch block logs and continues, which can silently emit an incomplete package.json and fail much later in command execution.
Suggested fix
- } catch (error) {
- console.error(
- `Error processing package.json.ejs for add-on ${addOn.id}:`,
- error,
- )
- }
+ } catch (error) {
+ throw new Error(
+ `Failed to process package.json template for add-on "${addOn.id}"`,
+ )
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues)) | |
| } catch (error) { | |
| console.error( | |
| `Error processing package.json.ejs for add-on ${addOn.id}:`, | |
| error, | |
| ) | |
| } | |
| try { | |
| addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues)) | |
| } catch (error) { | |
| throw new Error( | |
| `Failed to process package.json template for add-on "${addOn.id}"`, | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-package-json.ts` around lines 153 - 160, The catch
block in the try-catch statement around JSON.parse(render(addOn.packageTemplate,
templateValues)) only logs the error and continues execution, which allows
incomplete packageJSON data to persist and cause failures later. After logging
the error with console.error, throw the caught error to fail fast at the source
of the problem rather than silently continuing with invalid addOnPackageJSON
data.
| const templateValues = { | ||
| packageManager: options.packageManager, | ||
| projectName: options.projectName, | ||
| typescript: true, | ||
| tailwind: true, | ||
| js: 'ts', | ||
| jsx: 'tsx', |
There was a problem hiding this comment.
Use options.typescript for template flags instead of hardcoded TS values.
Template rendering is currently always TS-flavored (typescript/js/jsx), even when JS mode is selected, which can generate incorrect rendered content for non-TS projects.
Suggested fix
const templateValues = {
packageManager: options.packageManager,
projectName: options.projectName,
- typescript: true,
+ typescript: options.typescript,
tailwind: true,
- js: 'ts',
- jsx: 'tsx',
+ js: options.typescript ? 'ts' : 'js',
+ jsx: options.typescript ? 'tsx' : 'jsx',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const templateValues = { | |
| packageManager: options.packageManager, | |
| projectName: options.projectName, | |
| typescript: true, | |
| tailwind: true, | |
| js: 'ts', | |
| jsx: 'tsx', | |
| const templateValues = { | |
| packageManager: options.packageManager, | |
| projectName: options.projectName, | |
| typescript: options.typescript, | |
| tailwind: true, | |
| js: options.typescript ? 'ts' : 'js', | |
| jsx: options.typescript ? 'tsx' : 'jsx', |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-template-file.ts` around lines 127 - 133, The
templateValues object in the edge-template-file.ts file has hardcoded TypeScript
values (typescript: true, js: 'ts', jsx: 'tsx') that ignore the user's language
preference. Replace these hardcoded values with conditional values based on
options.typescript: set typescript to options.typescript, js to either 'ts' or
'js' depending on whether options.typescript is true, and jsx to either 'tsx' or
'jsx' depending on whether options.typescript is true. This ensures the correct
file extensions and flags are used for both TypeScript and JavaScript projects.
| return | ||
| } | ||
|
|
||
| let target = convertDotFilesAndPaths(file.replace('.ejs', '')) |
There was a problem hiding this comment.
Only strip .ejs when it is the file suffix.
Using file.replace('.ejs', '') can rename non-template files that merely contain .ejs in the middle of the filename.
Suggested fix
- let target = convertDotFilesAndPaths(file.replace('.ejs', ''))
+ const sourcePath = file.endsWith('.ejs') ? file.slice(0, -4) : file
+ let target = convertDotFilesAndPaths(sourcePath)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let target = convertDotFilesAndPaths(file.replace('.ejs', '')) | |
| const sourcePath = file.endsWith('.ejs') ? file.slice(0, -4) : file | |
| let target = convertDotFilesAndPaths(sourcePath) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/create/src/edge-template-file.ts` at line 183, The file.replace
method in the line with convertDotFilesAndPaths will remove all instances of
`.ejs` from the filename, not just the file extension at the end. Fix this by
modifying the replace call to use a regular expression pattern that matches
`.ejs` only at the end of the string using the `$` anchor, so that filenames
containing `.ejs` in the middle are left unchanged. This ensures only the actual
`.ejs` template extension is removed.
Summary
Adds a Worker-safe
@tanstack/create/edgeexport backed by a build-time generated framework/template/add-on manifest. The root@tanstack/createexport keeps the existing Node/CLI behavior.Details
@tanstack/create/edgeand@tanstack/create/manifestsubpath exports.@tanstack/create.Validation
npm testinpackages/createnpm run buildinpackages/createpnpm --filter @tanstack/cli buildpnpm --filter @tanstack/cli testnx run-many --target=build --parallel 5, unit tests, and blocking e2e tests@tanstack/create/edgenode:*,execa,ejs, ornew FunctionmarkersSummary by CodeRabbit
Release Notes
New Features
@tanstack/create/edgeexport enabling programmatic app generation in worker-safe runtimes without Node filesystem access (serverless workers, edge functions).Documentation